/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package injections;

import injections.annotations.PostInject;
import injections.annotations.Property;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * @author Tom
 */
public class InjectionAnalyzer {

    private static final Logger LOGGER = LoggerFactory.getLogger(InjectionAnalyzer.class);
    private static final Class<Property> PROPERTY = Property.class;

    /**
     * Class is injectable if it has non-parametric constructor
     *
     * @param cls class to inject
     * @return
     */
    public static boolean isInjectable(Class<?> cls) {
        Constructor<?>[] constructors = cls.getConstructors();
        boolean annotatedConstructor = false;
        for (Constructor<?> c : constructors) {
            // either parameterless constructor
            if (c.getParameterTypes().length == 0) {
                return true;
            }
            if (isAnnotatedConstructor(c)) {
                annotatedConstructor = true;
            }
        }
        return annotatedConstructor;
    }

    public static boolean isAnnotatedConstructor(Constructor c) {
        // or constructor(s) with all parameters annotated with @Property
        Annotation[][] cpa = c.getParameterAnnotations();
        boolean all = true;
        // browse each parameter
        for (Annotation[] pas : cpa) {
            boolean paramOk = false;
            for (Annotation a : pas) {
                if (a.annotationType() == Property.class) {
                    paramOk = true;
                }
            }
            if (!paramOk) {
                all = false;
            }
        }
        return all;
    }

    public static <T extends Annotation> List<Method> getMethodsWithAnnotation(Class<?> cls, Class<T> annotation) {
        List<Method> res = new ArrayList<>();
        Method[] methods = cls.getMethods();
        for (Method m : methods) {
            if (m.getAnnotation(annotation) != null) {
                res.add(m);
            }
        }
        return res;
    }

    /**
     * Return array of @PostInject methods to be executed in returned order
     *
     * @param cls
     * @return
     */
    public static Method[] getPostInjectMethods(Class<?> cls) {
        List<Method> foundMethods = new ArrayList<Method>();
        Class<?> currentClass = cls;
        while (currentClass != null) {
            LOGGER.debug("Looking for @PostInject methods in class " + currentClass.getName());
            // look for method with @PostInject annotation
            List<Method> methods = new ArrayList<>();
            Method[] declaredMethods = currentClass.getDeclaredMethods();
            for (Method m : declaredMethods) {
                if (m.getAnnotation(PostInject.class) != null) {
                    methods.add(m);
                }
            }
            // process
            LOGGER.debug("Found @PostInject methods " + methods);
            if (methods.size() > 1) {
                throw new IllegalArgumentException("Multiple @PostInject methods found!");
            }
            for (Method m : methods) {
                if (!Modifier.isPrivate(m.getModifiers())) {
                    LOGGER.warn("Public @PostInject method " + m.getName() + " at " + currentClass.getName() + " - use PRIVATE methods!");
                }
                m.setAccessible(true);
                if (m.getParameterTypes().length != 0) {
                    throw new IllegalArgumentException("@PostInject method " + m + " must have no parameters!");
                }
            }
            if (!methods.isEmpty()) {
                Method m = methods.get(0);
                LOGGER.debug("Found method " + m);
                foundMethods.add(m);
            }
            // proceed to super class
            currentClass = currentClass.getSuperclass();
        }
        // reverse order
        Collections.reverse(foundMethods);
        return foundMethods.toArray(new Method[0]);
    }

    public static String getInjectablePropName(Field f) {
        Property an = f.getAnnotation(PROPERTY);
        if (an == null) {
            throw new IllegalArgumentException("Not injectable! " + f);
        }
        return f.getName();
    }

    public static <T extends Annotation> List<Field> getFieldsWithAnnotation(Class<?> cls, Class<T> annotation) {
        List<Field> fields = new ArrayList<>();
        while (cls != null) {
            Field[] declaredFields = cls.getDeclaredFields();
            for (Field f : declaredFields) {
                if (f.getAnnotation(annotation) != null) {
                    fields.add(f);
                }
            }
            cls = cls.getSuperclass();
        }
        return fields;
    }

    public static boolean isPropertyField(Field f) {
        Property annotation = f.getAnnotation(PROPERTY);
        if (annotation != null) {
            return true;
        } else {
            return false;
        }
    }

    public static boolean isArray(Class<?> type) {
        if (type.isArray()) {
            return true;
        } else {
            return false;
        }
    }

    public static boolean isArray(Field f) {
        Class<?> type = f.getType();
        return isArray(type);
    }

    public static Class<?> getCollectionType(Field f) {
        ParameterizedType genericType = (ParameterizedType) f.getGenericType();
        Class<?> cls = (Class<?>) genericType.getActualTypeArguments()[0];
        return cls;
    }

    public static boolean isCollection(Class<?> type) {
        if (Collection.class.isAssignableFrom(type)) {
            return true;
        } else {
            return false;
        }
    }

    public static boolean isCollection(Field f) {
        Class<?> type = f.getType();
        return isCollection(type);
    }

    public static boolean isList(Field f) {
        Class<?> type = f.getType();
        if (type.isAssignableFrom(List.class)) {
            return true;
        } else {
            return false;
        }
    }

    public static boolean isOptional(Field f) {
        if (!isPropertyField(f)) {
            throw new IllegalArgumentException("Field " + f + " is not a property field!");
        }
        Property an = f.getAnnotation(PROPERTY);
        return an.optional();
    }

    public static Class getArrayMembersClass(Field f) {
        Class<?> cls = f.getType();
        return cls.getComponentType();
    }

    public static Class getArrayMembersClass(Class<?> cls) {
        return cls.getComponentType();
    }

    public static Class getListMembersClass(Field f) {
        if (!isList(f)) {
            throw new IllegalArgumentException("Field " + f + " is not a List");
        }
        Type genericType = f.getGenericType();
        ParameterizedType pt = (ParameterizedType) genericType;
        Type[] typeArgs = pt.getActualTypeArguments();
        Class<?> cls = (Class<?>) typeArgs[0];
        return cls;
    }


}
